package com.daffodil.util;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.commons.io.IOUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import lombok.extern.slf4j.Slf4j;

/**
 * 通用http发送方法
 * 
 * @author yweijian
 * @date 2019年8月18日
 * @version 1.0
 */
@Slf4j
public class HttpUtils {

    /**
     * 向指定 URL 发送GET方法的请求
     * <br> 默认请求头：
     * <br> accept = *\/*
     * <br> connection = Keep-Alive
     * <br> Cache-Control = no-cache
     * <br> user-agent = Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)
     * <br> Accept-Charset = UTF-8
     * <br> Content-Type = application/json;charset=UTF-8
     * @param url 发送请求的 URL
     * @return 所代表远程资源的响应结果
     */
    public static HttpResponseEntity sendGet(String url) {
        return sendGet(url, null, null);
    }

    /**
     * 向指定 URL 发送GET方法的请求
     * <br> 默认请求头：
     * <br> accept = *\/*
     * <br> connection = Keep-Alive
     * <br> Cache-Control = no-cache
     * <br> user-agent = Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)
     * <br> Accept-Charset = UTF-8
     * <br> Content-Type = application/json;charset=UTF-8
     * @param url 发送请求的 URL
     * @param params 请求参数，请求参数应该是 name1=value1&name2=value2 的形式
     * @return 所代表远程资源的响应结果
     */
    public static HttpResponseEntity sendGet(String url, String params) {
        return sendGet(url, params, null);
    }

    /**
     * 向指定 URL 发送GET方法的请求
     * <br> 默认请求头：
     * <br> accept = *\/*
     * <br> connection = Keep-Alive
     * <br> Cache-Control = no-cache
     * <br> user-agent = Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)
     * <br> Accept-Charset = UTF-8
     * <br> Content-Type = application/json;charset=UTF-8
     * @param url 发送请求的 URL
     * @param params 请求参数，请求参数应该是 name1=value1&name2=value2 的形式
     * @param header 请求头Map<String, String>
     * @return 所代表远程资源的响应结果
     */
    public static HttpResponseEntity sendGet(String url, String params, Map<String, String> headers) {
        return sendRequest(HttpMethod.GET, url, params, null, headers);
    }

    /**
     * 向指定 URL 发送POST方法的请求
     * <br> 默认请求头：
     * <br> accept = *\/*
     * <br> connection = Keep-Alive
     * <br> Cache-Control = no-cache
     * <br> user-agent = Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)
     * <br> Accept-Charset = UTF-8
     * <br> Content-Type = application/json;charset=UTF-8
     * @param url 发送请求的 URL
     * @return 所代表远程资源的响应结果
     */
    public static HttpResponseEntity sendPost(String url) {
        return sendPost(url, null, "{}", null);
    }

    /**
     * 向指定 URL 发送POST方法的请求
     * <br> 默认请求头：
     * <br> accept = *\/*
     * <br> connection = Keep-Alive
     * <br> Cache-Control = no-cache
     * <br> user-agent = Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)
     * <br> Accept-Charset = UTF-8
     * <br> Content-Type = application/json;charset=UTF-8
     * @param url 发送请求的 URL
     * @param params 请求参数，请求参数应该是 name1=value1&name2=value2 的形式
     * @return 所代表远程资源的响应结果
     */
    public static HttpResponseEntity sendPost(String url, String params) {
        return sendPost(url, params, "{}", null);
    }

    /**
     * 向指定 URL 发送POST方法的请求
     * <br> 默认请求头：
     * <br> accept = *\/*
     * <br> connection = Keep-Alive
     * <br> Cache-Control = no-cache
     * <br> user-agent = Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)
     * <br> Accept-Charset = UTF-8
     * <br> Content-Type = application/json;charset=UTF-8
     * @param url 发送请求的 URL
     * @param params 请求参数，请求参数应该是 name1=value1&name2=value2 的形式
     * @param body 请求体参数text/json
     * @return 所代表远程资源的响应结果
     */
    public static HttpResponseEntity sendPost(String url, String params, String body) {
        return sendPost(url, params, body, null);
    }

    /**
     * 向指定 URL 发送POST方法的请求
     * <br> 默认请求头：
     * <br> accept = *\/*
     * <br> connection = Keep-Alive
     * <br> Cache-Control = no-cache
     * <br> user-agent = Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)
     * <br> Accept-Charset = UTF-8
     * <br> Content-Type = application/json;charset=UTF-8
     * @param url 发送请求的 URL
     * @param params 请求参数，请求参数应该是 name1=value1&name2=value2 的形式
     * @param body 请求体参数text/json
     * @param header 请求头Map<String, String>
     * @return 所代表远程资源的响应结果
     */
    public static HttpResponseEntity sendPost(String url, String params, String body, Map<String, String> headers) {
        return sendRequest(HttpMethod.POST, url, params, body, headers);
    }

    /**
     * 向指定 URL 发送请求
     * <br> 默认请求头：
     * <br> accept = *\/*
     * <br> connection = Keep-Alive
     * <br> Cache-Control = no-cache
     * <br> user-agent = Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)
     * <br> Accept-Charset = UTF-8
     * <br> Content-Type = application/json;charset=UTF-8
     * @param method 发送请求的方法
     * @param url 发送请求的 URL
     * @param params 请求参数，请求参数应该是 name1=value1&name2=value2 的形式
     * @param body 请求体参数text/json
     * @param header 请求头Map<String, String>
     * @return 所代表远程资源的响应结果
     */
    public static HttpResponseEntity sendRequest(HttpMethod method, String url, String params, String body, Map<String, String> headers) {
        if(StringUtils.isNotEmpty(url) && url.toLowerCase().startsWith("https://")) {
            return sendSSLRequest(method, url, params, body, headers);
        }
        HttpResponseEntity responseEntity = new HttpResponseEntity();
        HttpResponseData responseData = new HttpResponseData();
        String responseBody = null;
        HttpStatus httpStatus = null;
        InputStream in = null;
        OutputStream out = null;
        HttpURLConnection connection = null;
        try {
            url = url + (StringUtils.isNotEmpty(params) ? "?" + params : "");
            URL uri = new URL(url);

            responseEntity.setMethod(method);
            responseEntity.setUrl(uri);
            responseEntity.setParams(params);
            responseEntity.setBody(body);

            connection  = (HttpURLConnection) uri.openConnection();
            connection.setRequestMethod(method.name());
            connection.setConnectTimeout(30 * 1000);
            connection.setDoOutput(true);
            connection.setDoInput(true);

            Map<String, String> defaultHeader = buildDefaultRequestHeaders(headers);
            responseEntity.setHeaders(defaultHeader);
            for(Map.Entry<String, String> entry : defaultHeader.entrySet()) {
                connection.setRequestProperty(entry.getKey(), entry.getValue());
            }

            String charset = defaultHeader.get("Accept-Charset");
            charset = StringUtils.isNotEmpty(charset) ? charset : StandardCharsets.UTF_8.toString();

            if(HttpMethod.POST.name().equals(method.name()) || HttpMethod.PUT.name().equals(method.name())) {
                body = StringUtils.isNotEmpty(body) ? body : "{}";
                out = connection.getOutputStream();
                IOUtils.write(body, out, charset);
            }

            connection.connect();
            responseData.setHeaders(connection.getHeaderFields());
            httpStatus = HttpStatus.valueOf(connection.getResponseCode());
            in = connection.getInputStream();
            if(null != in) {
                responseBody = IOUtils.toString(in, charset);
                responseData.setBody(responseBody);
            }

            responseData.setStatus(httpStatus.value());
            responseData.setMessage(connection.getResponseMessage());
        } catch (ConnectException e) {
            responseData.setStatus(httpStatus != null ? httpStatus.value() : HttpStatus.INTERNAL_SERVER_ERROR.value());
            responseData.setMessage(e.getMessage());
            log.error("调用HttpUtils.sendRequest ConnectException, status={}, method={}, url={}, params={}, body={}, headers={}", responseData.getStatus(), method.name(), url, params, body, headers, e);
        } catch (SocketTimeoutException e) {
            responseData.setStatus(httpStatus != null ? httpStatus.value() : HttpStatus.REQUEST_TIMEOUT.value());
            responseData.setMessage(e.getMessage());
            log.error("调用HttpUtils.sendRequest SocketTimeoutException, status={}, method={}, url={}, params={}, body={}, headers={}", responseData.getStatus(), method.name(), url, params, body, headers, e);
        } catch (IOException e) {
            responseData.setStatus(httpStatus != null ? httpStatus.value() : HttpStatus.INTERNAL_SERVER_ERROR.value());
            responseData.setMessage(e.getMessage());
            log.error("调用HttpUtils.sendRequest IOException, status={}, method={}, url={}, params={}, body={}, headers={}", responseData.getStatus(), method.name(), url, params, body, headers, e);
        } catch (Exception e) {
            responseData.setStatus(httpStatus != null ? httpStatus.value() : HttpStatus.INTERNAL_SERVER_ERROR.value());
            responseData.setMessage(e.getMessage());
            log.error("调用HttpsUtil.sendRequest Exception, status={}, method={}, url={}, params={}, body={}, headers={}", responseData.getStatus(), method.name(), url, params, body, headers, e);
        }finally {
            if(in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    log.error(e.getMessage(), e);
                }
            }
            if(out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    log.error(e.getMessage(), e);
                }
            }
            if(connection != null) {
                connection.disconnect();
            }
            if(log.isInfoEnabled()) {
                String json = StringUtils.isNotEmpty(responseBody) ? JacksonUtils.toJSONString(responseBody) : null;
                String resp = StringUtils.isNotEmpty(json) ? json : (responseBody != null && responseBody.length() > 1024) ? responseBody.substring(0, 1024) : responseBody;
                log.info("调用HttpUtils.sendRequest Response, status={}, method={}, url={}, params={}, body={}, headers={}, result={}", responseData.getStatus(), method.name(), url, params, body, headers, resp);
            }
        }
        responseEntity.setData(responseData);
        return responseEntity;
    }

    /**
     * 向指定 URL 发送GET方法的请求
     * <br> 默认请求头：
     * <br> accept = *\/*
     * <br> connection = Keep-Alive
     * <br> Cache-Control = no-cache
     * <br> user-agent = Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)
     * <br> Accept-Charset = UTF-8
     * <br> Content-Type = application/json;charset=UTF-8
     * @param url 发送请求的 URL
     * @return 所代表远程资源的响应结果
     */
    public static HttpResponseEntity sendSSLGet(String url) {
        return sendGet(url, null, null);
    }

    /**
     * 向指定 URL 发送GET方法的请求
     * <br> 默认请求头：
     * <br> accept = *\/*
     * <br> connection = Keep-Alive
     * <br> Cache-Control = no-cache
     * <br> user-agent = Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)
     * <br> Accept-Charset = UTF-8
     * <br> Content-Type = application/json;charset=UTF-8
     * @param url 发送请求的 URL
     * @param params 请求参数，请求参数应该是 name1=value1&name2=value2 的形式
     * @return 所代表远程资源的响应结果
     */
    public static HttpResponseEntity sendSSLGet(String url, String params) {
        return sendGet(url, params, null);
    }

    /**
     * 向指定 URL 发送GET方法的请求
     * <br> 默认请求头：
     * <br> accept = *\/*
     * <br> connection = Keep-Alive
     * <br> Cache-Control = no-cache
     * <br> user-agent = Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)
     * <br> Accept-Charset = UTF-8
     * <br> Content-Type = application/json;charset=UTF-8
     * @param url 发送请求的 URL
     * @param params 请求参数，请求参数应该是 name1=value1&name2=value2 的形式
     * @param header 请求头Map<String, String>
     * @return 所代表远程资源的响应结果
     */
    public static HttpResponseEntity sendSSLGet(String url, String params, Map<String, String> headers) {
        return sendSSLRequest(HttpMethod.GET, url, params, null, headers);
    }

    /**
     * 向指定 URL 发送POST方法的请求
     * <br> 默认请求头：
     * <br> accept = *\/*
     * <br> connection = Keep-Alive
     * <br> Cache-Control = no-cache
     * <br> user-agent = Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)
     * <br> Accept-Charset = UTF-8
     * <br> Content-Type = application/json;charset=UTF-8
     * @param url 发送请求的 URL
     * @return 所代表远程资源的响应结果
     */
    public static HttpResponseEntity sendSSLPost(String url) {
        return sendSSLPost(url, null, "{}", null);
    }

    /**
     * 向指定 URL 发送POST方法的请求
     * <br> 默认请求头：
     * <br> accept = *\/*
     * <br> connection = Keep-Alive
     * <br> Cache-Control = no-cache
     * <br> user-agent = Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)
     * <br> Accept-Charset = UTF-8
     * <br> Content-Type = application/json;charset=UTF-8
     * @param url 发送请求的 URL
     * @param params 请求参数，请求参数应该是 name1=value1&name2=value2 的形式。
     * @return 所代表远程资源的响应结果
     */
    public static HttpResponseEntity sendSSLPost(String url, String params) {
        return sendSSLPost(url, params, "{}", null);
    }

    /**
     * 向指定 URL 发送POST方法的请求
     * <br> 默认请求头：
     * <br> accept = *\/*
     * <br> connection = Keep-Alive
     * <br> Cache-Control = no-cache
     * <br> user-agent = Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)
     * <br> Accept-Charset = UTF-8
     * <br> Content-Type = application/json;charset=UTF-8
     * @param url 发送请求的 URL
     * @param params 请求参数，请求参数应该是 name1=value1&name2=value2 的形式。
     * @param body 请求体参数text/json
     * @return 所代表远程资源的响应结果
     */
    public static HttpResponseEntity sendSSLPost(String url, String params, String body) {
        return sendSSLPost(url, params, body, null);
    }

    /**
     * 向指定 URL 发送POST方法的请求
     * <br> 默认请求头：
     * <br> accept = *\/*
     * <br> connection = Keep-Alive
     * <br> Cache-Control = no-cache
     * <br> user-agent = Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)
     * <br> Accept-Charset = UTF-8
     * <br> Content-Type = application/json;charset=UTF-8
     * @param url 发送请求的 URL
     * @param params 请求参数，请求参数应该是 name1=value1&name2=value2 的形式。
     * @param body 请求体参数text/json
     * @param header 请求头Map<String, String>
     * @return 所代表远程资源的响应结果
     */
    public static HttpResponseEntity sendSSLPost(String url, String params, String body, Map<String, String> headers) {
        return sendSSLRequest(HttpMethod.POST, url, params, body, headers);
    }

    /**
     * 向指定 URL 发送请求
     * <br> 默认请求头：
     * <br> accept = *\/*
     * <br> connection = Keep-Alive
     * <br> Cache-Control = no-cache
     * <br> user-agent = Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)
     * <br> Accept-Charset = UTF-8
     * <br> Content-Type = application/json;charset=UTF-8
     * @param method 发送请求的方法
     * @param url 发送请求的 URL
     * @param params 请求参数，请求参数应该是 name1=value1&name2=value2 的形式。
     * @param body 请求体参数text/json
     * @param header 请求头Map<String, String>
     * @return 所代表远程资源的响应结果
     */
    public static HttpResponseEntity sendSSLRequest(HttpMethod method, String url, String params, String body, Map<String, String> headers) {
        HttpResponseEntity responseEntity = new HttpResponseEntity();
        HttpResponseData responseData = new HttpResponseData();
        String responseBody = null;
        HttpStatus httpStatus = null;
        InputStream in = null;
        OutputStream out = null;
        HttpsURLConnection connection = null;
        try {
            url = url + (StringUtils.isNotEmpty(params) ? "?" + params : "");
            URL uri = new URL(url);

            responseEntity.setMethod(method);
            responseEntity.setUrl(uri);
            responseEntity.setParams(params);
            responseEntity.setBody(body);

            connection  = (HttpsURLConnection) uri.openConnection();
            connection.setRequestMethod(method.name());
            connection.setConnectTimeout(30 * 1000);
            connection.setDoOutput(true);
            connection.setDoInput(true);

            SSLContext context = SSLContext.getInstance("SSL");
            context.init(null, new TrustManager[] { new TrustAnyTrustManager() }, new java.security.SecureRandom());
            connection.setSSLSocketFactory(context.getSocketFactory());
            connection.setHostnameVerifier(new TrustAnyHostnameVerifier());

            Map<String, String> defaultHeader = buildDefaultRequestHeaders(headers);
            responseEntity.setHeaders(defaultHeader);
            for(Map.Entry<String, String> entry : defaultHeader.entrySet()) {
                connection.setRequestProperty(entry.getKey(), entry.getValue());
            }

            String charset = defaultHeader.get("Accept-Charset");
            charset = StringUtils.isNotEmpty(charset) ? charset : StandardCharsets.UTF_8.toString();

            if(HttpMethod.POST.name().equals(method.name()) || HttpMethod.PUT.name().equals(method.name())) {
                body = StringUtils.isNotEmpty(body) ? body : "{}";
                out = connection.getOutputStream();
                IOUtils.write(body, out, charset);
            }

            connection.connect();
            responseData.setHeaders(connection.getHeaderFields());
            httpStatus = HttpStatus.valueOf(connection.getResponseCode());
            in = connection.getInputStream();
            if(null != in) {
                responseBody = IOUtils.toString(in, charset);
                responseData.setBody(responseBody);
            }

            responseData.setStatus(httpStatus.value());
            responseData.setMessage(connection.getResponseMessage());
        } catch (ConnectException e) {
            responseData.setStatus(httpStatus != null ? httpStatus.value() : HttpStatus.INTERNAL_SERVER_ERROR.value());
            responseData.setMessage(e.getMessage());
            log.error("调用HttpUtils.sendSSLRequest ConnectException, status={}, method={}, url={}, params={}, body={}, headers={}", responseData.getStatus(), method.name(), url, params, body, headers, e);
        } catch (SocketTimeoutException e) {
            responseData.setStatus(httpStatus != null ? httpStatus.value() : HttpStatus.REQUEST_TIMEOUT.value());
            responseData.setMessage(e.getMessage());
            log.error("调用HttpUtils.sendSSLRequest SocketTimeoutException, status={}, method={}, url={}, params={}, body={}, headers={}", responseData.getStatus(), method.name(), url, params, body, headers, e);
        } catch (IOException e) {
            responseData.setStatus(httpStatus != null ? httpStatus.value() : HttpStatus.INTERNAL_SERVER_ERROR.value());
            responseData.setMessage(e.getMessage());
            log.error("调用HttpUtils.sendSSLRequest IOException, status={}, method={}, url={}, params={}, body={}, headers={}", responseData.getStatus(), method.name(), url, params, body, headers, e);
        } catch (Exception e) {
            responseData.setStatus(httpStatus != null ? httpStatus.value() : HttpStatus.INTERNAL_SERVER_ERROR.value());
            responseData.setMessage(e.getMessage());
            log.error("调用HttpsUtil.sendSSLRequest Exception, status={}, method={}, url={}, params={}, body={}, headers={}", responseData.getStatus(), method.name(), url, params, body, headers, e);
        }finally {
            if(in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    log.error(e.getMessage(), e);
                }
            }
            if(out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    log.error(e.getMessage(), e);
                }
            }
            if(connection != null) {
                connection.disconnect();
            }
            if(log.isInfoEnabled()) {
                String json = StringUtils.isNotEmpty(responseBody) ? JacksonUtils.toJSONString(responseBody) : null;
                String resp = StringUtils.isNotEmpty(json) ? json : (responseBody != null && responseBody.length() > 1024) ? responseBody.substring(0, 1024) : responseBody;
                log.info("调用HttpUtils.sendSSLRequest Response, status={}, method={}, url={}, params={}, body={}, headers={}, result={}", responseData.getStatus(), method.name(), url, params, body, headers, resp);
            }
        }
        responseEntity.setData(responseData);
        return responseEntity;
    }

    /**
     * <br>默认请求头：
     * <br> accept = *\/*
     * <br> connection = Keep-Alive
     * <br> Cache-Control = no-cache
     * <br> user-agent = Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)
     * <br> Accept-Charset = UTF-8
     * <br> Content-Type = application/json;charset=UTF-8
     * @param header 请求头
     * @return 请求头
     */
    public static Map<String, String> buildDefaultRequestHeaders(Map<String, String> headers){
        Map<String, String> defaultHeaders = new HashMap<String, String>();
        defaultHeaders.put("accept", "*/*");
        defaultHeaders.put("connection", "Keep-Alive");
        defaultHeaders.put("Cache-Control", "no-cache");
        defaultHeaders.put("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:99.0) Gecko/20100101 Firefox/99.0)");
        defaultHeaders.put("Accept-Charset", StandardCharsets.UTF_8.name());
        defaultHeaders.put("Content-Type", MediaType.APPLICATION_JSON_UTF8_VALUE);
        if(headers != null && !headers.isEmpty()) {
            headers.forEach((key, value) -> {
                defaultHeaders.put(key, value);
            });
        }
        return defaultHeaders;
    }

    private static class TrustAnyTrustManager implements X509TrustManager {
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) {
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) {
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[] {};
        }
    }

    private static class TrustAnyHostnameVerifier implements HostnameVerifier {
        @Override
        public boolean verify(String hostname, SSLSession session) {
            return true;
        }
    }

}